package org.ly.uap.client.jaas;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.ly.uap.client.authentication.SimpleGroup;
import org.ly.uap.client.authentication.SimplePrincipal;
import org.ly.uap.client.util.CommonUtils;
import org.ly.uap.client.util.ReflectUtils;
import org.ly.uap.client.validation.Assertion;
import org.ly.uap.client.validation.TicketValidator;

public class CasLoginModule
        implements LoginModule {
    public static final String LOGIN_NAME = "javax.security.auth.login.name";
    public static final String DEFAULT_PRINCIPAL_GROUP_NAME = "CallerPrincipal";
    public static final String DEFAULT_ROLE_GROUP_NAME = "Roles";
    public static final int DEFAULT_CACHE_TIMEOUT = 480;
    protected static final Map<TicketCredential, Assertion> ASSERTION_CACHE = new HashMap();

    protected static Executor cacheCleanerExecutor = Executors.newSingleThreadExecutor();

    protected final Log log = LogFactory.getLog(getClass());
    protected Subject subject;
    protected CallbackHandler callbackHandler;
    protected TicketValidator ticketValidator;
    protected String service;
    protected Assertion assertion;
    protected TicketCredential ticket;
    protected Map<String, Object> sharedState;
    protected String[] defaultRoles;
    protected Set<String> roleAttributeNames = new HashSet();

    protected String principalGroupName = "CallerPrincipal";

    protected String roleGroupName = "Roles";
    protected boolean cacheAssertions;
    protected int cacheTimeout = 480;

    public void initialize(Subject subject, CallbackHandler handler, Map<String, ?> state, Map<String, ?> options) {
        this.assertion = null;
        this.callbackHandler = handler;
        this.subject = subject;
        this.sharedState = new HashMap(state);

        String ticketValidatorClass = null;

        for (String key : options.keySet()) {
            this.log.trace("Processing option " + key);
            if ("service".equals(key)) {
                this.service = ((String) options.get(key));
                this.log.debug("Set service=" + this.service);
            } else if ("ticketValidatorClass".equals(key)) {
                ticketValidatorClass = (String) options.get(key);
                this.log.debug("Set ticketValidatorClass=" + ticketValidatorClass);
            } else if ("defaultRoles".equals(key)) {
                String roles = (String) options.get(key);
                this.log.trace("Got defaultRoles value " + roles);
                this.defaultRoles = roles.split(",\\s*");
                this.log.debug("Set defaultRoles=" + Arrays.asList(this.defaultRoles));
            } else if ("roleAttributeNames".equals(key)) {
                String attrNames = (String) options.get(key);
                this.log.trace("Got roleAttributeNames value " + attrNames);
                String[] attributes = attrNames.split(",\\s*");
                this.roleAttributeNames.addAll(Arrays.asList(attributes));
                this.log.debug("Set roleAttributeNames=" + this.roleAttributeNames);
            } else if ("principalGroupName".equals(key)) {
                this.principalGroupName = ((String) options.get(key));
                this.log.debug("Set principalGroupName=" + this.principalGroupName);
            } else if ("roleGroupName".equals(key)) {
                this.roleGroupName = ((String) options.get(key));
                this.log.debug("Set roleGroupName=" + this.roleGroupName);
            } else if ("cacheAssertions".equals(key)) {
                this.cacheAssertions = Boolean.parseBoolean((String) options.get(key));
                this.log.debug("Set cacheAssertions=" + this.cacheAssertions);
            } else if ("cacheTimeout".equals(key)) {
                this.cacheTimeout = Integer.parseInt((String) options.get(key));
                this.log.debug("Set cacheTimeout=" + this.cacheTimeout);
            }
        }

        if (this.cacheAssertions) {
            cacheCleanerExecutor.execute(new CacheCleaner());
        }

        CommonUtils.assertNotNull(ticketValidatorClass, "ticketValidatorClass is required.");
        this.ticketValidator = createTicketValidator(ticketValidatorClass, options);
    }

    public boolean login() throws LoginException {
        this.log.debug("Performing login.");
        NameCallback serviceCallback = new NameCallback("service");
        PasswordCallback ticketCallback = new PasswordCallback("ticket", false);
        try {
            this.callbackHandler.handle(new Callback[]{ticketCallback, serviceCallback});
        } catch (IOException e) {
            this.log.info("Login failed due to IO exception in callback handler: " + e);
            throw ((LoginException) new LoginException("IO exception in callback handler: " + e).initCause(e));
        } catch (UnsupportedCallbackException e) {
            this.log.info("Login failed due to unsupported callback: " + e);
            throw ((LoginException) new LoginException("Callback handler does not support PasswordCallback and TextInputCallback.").initCause(e));
        }

        if (ticketCallback.getPassword() != null) {
            this.ticket = new TicketCredential(new String(ticketCallback.getPassword()));
            String service = CommonUtils.isNotBlank(serviceCallback.getName()) ? serviceCallback.getName() : this.service;

            if (this.cacheAssertions) {
                synchronized (ASSERTION_CACHE) {
                    if (ASSERTION_CACHE.get(this.ticket) != null) {
                        this.log.debug("Assertion found in cache.");
                        this.assertion = ((Assertion) ASSERTION_CACHE.get(this.ticket));
                    }
                }
            }

            if (this.assertion == null) {
                this.log.debug("CAS assertion is null; ticket validation required.");
                if (CommonUtils.isBlank(service)) {
                    this.log.info("Login failed because required CAS service parameter not provided.");
                    throw new LoginException("Neither login module nor callback handler provided required service parameter.");
                }
                try {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Attempting ticket validation with service=" + service + " and ticket=" + this.ticket);
                    }
                    this.assertion = this.ticketValidator.validate(this.ticket.getName(), service);
                } catch (Exception e) {
                    this.log.info("Login failed due to CAS ticket validation failure: " + e);
                    throw ((LoginException) new LoginException("CAS ticket validation failed: " + e).initCause(e));
                }
            }
            this.log.info("Login succeeded.");
        } else {
            this.log.info("Login failed because callback handler did not provide CAS ticket.");
            throw new LoginException("Callback handler did not provide CAS ticket.");
        }
        return true;
    }

    public boolean abort() throws LoginException {
        if (this.ticket != null) {
            this.ticket = null;
        }
        if (this.assertion != null) {
            this.assertion = null;
        }
        return true;
    }

    public boolean commit() throws LoginException {
        if (this.assertion != null) {
            if (this.ticket != null)
                this.subject.getPrivateCredentials().add(this.ticket);
            else {
                throw new LoginException("Ticket credential not found.");
            }

            AssertionPrincipal casPrincipal = new AssertionPrincipal(this.assertion.getPrincipal().getName(), this.assertion);
            this.subject.getPrincipals().add(casPrincipal);

            Group principalGroup = new SimpleGroup(this.principalGroupName);
            principalGroup.addMember(casPrincipal);
            this.subject.getPrincipals().add(principalGroup);

            Group roleGroup = new SimpleGroup(this.roleGroupName);

            for (String defaultRole : this.defaultRoles) {
                roleGroup.addMember(new SimplePrincipal(defaultRole));
            }

            Map<String, Object> attributes = this.assertion.getPrincipal().getAttributes();
            for (String key : attributes.keySet()) {
                if (!this.roleAttributeNames.contains(key))
                    continue;
                Object value = attributes.get(key);
                if ((value instanceof Collection))
                    for (Iterator localIterator2 = ((Collection) value).iterator(); localIterator2.hasNext(); ) {
                        Object o = localIterator2.next();
                        roleGroup.addMember(new SimplePrincipal(o.toString()));
                    }
                else {
                    roleGroup.addMember(new SimplePrincipal(value.toString()));
                }
            }

            this.subject.getPrincipals().add(roleGroup);

            this.sharedState.put("javax.security.auth.login.name", new Object());

            if ((this.log.isDebugEnabled()) &&
                    (this.log.isDebugEnabled())) {
                this.log.debug("Created JAAS subject with principals: " + this.subject.getPrincipals());
            }

            if (this.cacheAssertions) {
                if (this.log.isDebugEnabled()) {
                    this.log.debug("Caching assertion for principal " + this.assertion.getPrincipal());
                }
                ASSERTION_CACHE.put(this.ticket, this.assertion);
            }

        } else if (this.ticket != null) {
            this.ticket = null;
        }

        return true;
    }

    public boolean logout() throws LoginException {
        this.log.debug("Performing logout.");

        removePrincipalsOfType(AssertionPrincipal.class);
        removePrincipalsOfType(SimplePrincipal.class);
        removePrincipalsOfType(SimpleGroup.class);

        removeCredentialsOfType(TicketCredential.class);

        this.log.info("Logout succeeded.");
        return true;
    }

    private TicketValidator createTicketValidator(String className, Map<String, ?> propertyMap) {
        CommonUtils.assertTrue(propertyMap.containsKey("casServerUrlPrefix"), "Required property casServerUrlPrefix not found.");

        Class validatorClass = ReflectUtils.loadClass(className);
        TicketValidator validator = (TicketValidator) ReflectUtils.newInstance(validatorClass, new Object[]{propertyMap.get("casServerUrlPrefix")});
        try {
            BeanInfo info = Introspector.getBeanInfo(validatorClass);

            for (String property : propertyMap.keySet())
                if (!"casServerUrlPrefix".equals(property)) {
                    this.log.debug("Attempting to set TicketValidator property " + property);
                    String value = (String) propertyMap.get(property);
                    PropertyDescriptor pd = ReflectUtils.getPropertyDescriptor(info, property);
                    if (pd != null) {
                        ReflectUtils.setProperty(property, convertIfNecessary(pd, value), validator, info);
                        this.log.debug("Set " + property + "=" + value);
                    } else {
                        this.log.warn("Cannot find property " + property + " on " + className);
                    }
                }
        } catch (IntrospectionException e) {
            throw new RuntimeException("Error getting bean info for " + validatorClass, e);
        }

        return validator;
    }

    private static Object convertIfNecessary(PropertyDescriptor pd, String value) {
        if (String.class.equals(pd.getPropertyType()))
            return value;
        if (Boolean.TYPE.equals(pd.getPropertyType()))
            return Boolean.valueOf(value);
        if (Integer.TYPE.equals(pd.getPropertyType()))
            return new Integer(value);
        if (Long.TYPE.equals(pd.getPropertyType())) {
            return new Long(value);
        }
        throw new IllegalArgumentException("No conversion strategy exists for property " + pd.getName() + " of type " + pd.getPropertyType());
    }

    private void removePrincipalsOfType(Class<? extends Principal> clazz) {
        this.subject.getPrincipals().removeAll(this.subject.getPrincipals(clazz));
    }

    private void removeCredentialsOfType(Class<? extends Principal> clazz) {
        this.subject.getPrivateCredentials().removeAll(this.subject.getPrivateCredentials(clazz));
    }

    private class CacheCleaner implements Runnable {
        private CacheCleaner() {
        }

        public void run() {
            if (CasLoginModule.this.log.isDebugEnabled()) {
                CasLoginModule.this.log.debug("Cleaning assertion cache of size " + CasLoginModule.ASSERTION_CACHE.size());
            }
            Iterator iter =
                    CasLoginModule.ASSERTION_CACHE.entrySet().iterator();
            Calendar cutoff = Calendar.getInstance();
            cutoff.add(12, -CasLoginModule.this.cacheTimeout);
            while (iter.hasNext()) {
                Assertion assertion = (Assertion) ((Entry) iter.next()).getValue();
                Calendar created = Calendar.getInstance();
                created.setTime(assertion.getValidFromDate());
                if (created.before(cutoff)) {
                    if (CasLoginModule.this.log.isDebugEnabled()) {
                        CasLoginModule.this.log.debug("Removing expired assertion for principal " + assertion.getPrincipal());
                    }
                    iter.remove();
                }
            }
        }
    }
}
